<template>
  <div class="h-100 d-flex flex-dir-column">
    <app-tool-menu :tname="$t('app.menu.tool.wsServer')"/>

    <template v-if="hasInited">
      <!-- options -->
      <div class="p-2 bg-light">
        <a-form layout="inline">
          <a-form-item>
            <a-input-group compact>
              <!-- protocol -->
              <a-select 
                style="width: 60px" 
                v-model="protocol" 
                :showArrow="false" 
                :disabled="isListening"
                @change="actionOptionUpdate(null)"
              >
                <a-select-option value="ws">ws://</a-select-option>
                <a-select-option value="wss">wss://</a-select-option>
              </a-select>
              
              <!-- host -->
              <a-input 
                style="width:150px;" 
                v-model="host" 
                :disabled="isListening"
                @change="actionOptionUpdate(null)"
              />

              <a-input
                style=" width: 30px; border-left: 0; pointer-events: none; backgroundColor: #fff"
                placeholder=":"
                disabled
              />
              
              <!-- port -->
              <a-input 
                style="width:70px;" 
                v-model.number="port" 
                :disabled="isListening"
                @change="actionOptionUpdate(null)"
              />

              <a-input
                style=" width: 30px; border-left: 0; pointer-events: none; backgroundColor: #fff"
                placeholder="/"
                disabled
              />

              <!-- path -->
              <a-input 
                style="width:150px;" 
                v-model="path"
                :disabled="isListening" 
                @change="actionOptionUpdate(null)"
              />
            </a-input-group>
          </a-form-item>
          
          <!-- charset -->
          <a-form-item>
            <a-select 
              v-model="charset" 
              default-value="utf8" 
              style="width:120px" 
              :disabled="isListening"
              @change="actionOptionUpdate(null)"
            >
              <a-select-option v-for="(item, key) in $dict.items('CHARSET')" :key="key" :value="item.value"
              >{{item.name}}</a-select-option>
            </a-select>
          </a-form-item>

          <a-form-item>
            <!-- setting -->
            <modal-setting 
              :enable="'wss' === protocol && !isListening" 
              :options="options" 
              @ok="actionSettingOk"
            ></modal-setting>
            
            <a-button v-if="!isListening" type="primary" html-type="submit" @click="actionStart">
              {{$t('app.toolTcpServer.btnStart')}}
            </a-button>
            <a-button v-if="isListening" type="danger" html-type="submit" @click="actionStop">
              {{$t('app.toolTcpServer.btnStop')}}
            </a-button>
          </a-form-item>
        </a-form>
      </div>
      
      <!-- data viewer -->
      <div class="p-2 flex-grow-2 h-0 d-flex flex-dir-column">
        <a-empty 
          class="mt-5" 
          v-if="0 == Object.keys(clients)" 
          :description="$t('app.toolTcpServer.noClients')"
        />

        <template v-else>
          <a-row class="border-bottom pb-1">
            <a-col :span="12">
              {{$t('app.tool.viewerMode')}} : 
              <a-radio-group size="small" v-model="viewmode" @change="actionOptionUpdate(null)">
                <a-radio-button value="hex">HEX</a-radio-button>
                <a-radio-button value="text">TEXT</a-radio-button>
              </a-radio-group>

              <span class="ml-2">{{$t('app.tool.dataViewerMergeRecentlyReceiveData')}} : </span>
              <a-switch size="small" v-model="enableDataMerge" @change="actionOptionUpdate(null)" />
            </a-col>
            <a-col :span="12" class="text-right">
              <a-input 
                disabled 
                size="small" 
                class="text-center"
                style="width:160px;"
                :addon-before="$t('messages.send')" 
                :value="dataSizeSend.toLocaleString('en-US')" 
              ><a-icon slot="addonAfter" type="close-circle" @click="actionDataSizeSendReset" /></a-input>
              &nbsp;
              <a-input 
                disabled 
                size="small" 
                class="text-center"
                style="width:160px;"
                :addon-before="$t('messages.receive')" 
                :value="dataSizeReceive.toLocaleString('en-US')"
              ><a-icon slot="addonAfter" type="close-circle" @click="actionDataSizeReceiveReset" /></a-input>
            </a-col>
          </a-row>

          <a-tabs 
            id="clienttab"
            hide-add
            size="small" 
            type="editable-card" 
            tabPosition="right" 
            style="height: 100%;"
            @edit="actionClientTabEdit"
            @change="actionClientTabChange"
          >
            <a-tab-pane v-for="(client, key) in clients" :key="key" :forceRender="true">
              <span slot="tab">
                {{key}}
                <a-badge v-if="client.hasNewData" status="processing" />
                <a-badge v-else-if="client.isConnected" status="success" />
                <a-badge v-else status="default" />
              </span>

              <data-viewer 
                :ref="`dataViewer:${key}`"
                :viewmode="viewmode"
                :enable-data-merge="enableDataMerge"
                :client-key="key"
                @data-size-send-changed="(size,clientKey) => actionDataSizeChanged( size, 'dataSizeSend', clientKey )"
                @data-size-receive-changed="(size,clientKey) => actionDataSizeChanged( size, 'dataSizeReceive', clientKey )"
              ></data-viewer>
            </a-tab-pane>
          </a-tabs>
        </template>
      </div>

      <!-- data editor -->
      <mock-response-handler
        ref="mockResponseHandler"
        :enable="isListening"
        :tool-options="options"
        :charset="charset"
        @tool-option-update="actionOptionUpdate"
        @response-generated="actionMockResponseGenerated"
      ></mock-response-handler>
    </template>
  </div>
</template>
<script>
import ToolMixin from '../utils/ToolMixin.js'
import AppToolMenu from '../utils/AppToolMenu.vue'
import AppHelper from '../../utils/AppHelper.js'
import DataViewer from '../utils/mock-server/DataViewer.vue'
import MockResponseHandler from '../utils/mock-server/ResponseHandler.vue'
import MdbRuntimeVariable from '@/models/MdbRuntimeVariable.js'
import Common from '@/utils/Common.js'
import Dictionary from '@/utils/Dictionary.js'
import { Buffer } from 'buffer'
require('../../utils/Common.css');
import ModalSetting from './ModalSetting.vue'
export default {
    name : 'ToolWsServer',
    mixins : [ToolMixin],
    components : {
        'data-viewer' : DataViewer,
        'mock-response-handler' : MockResponseHandler,
        'modal-setting' : ModalSetting,
        'app-tool-menu' : AppToolMenu,
    },
    data() {
        return {
            options : {},
            
            protocol : 'ws',
            host : '127.0.0.1',
            port : 8899,
            path : '',
            charset : 'utf8',
            viewmode : 'text',
            enableDataMerge : true,
            sslKey : null,
            sslCert : null,

            httpsServer : null,
            isListening: false,
            server : null,
            clients : {},
            activeClientKey : null,
            sendCotnent : null,
            sendFormat : 'text',
            defaultViewerFormat : 'text',
            nlStyle : 'LF',
            hasInited : false,
            dataSizeSend : 0,
            dataSizeReceive : 0,
        };
    },
    mounted () {
        this.init();
    },
    beforeDestroy() {
        this.actionStop();
    },
    methods : {
        /**
         * init
         */
        async init() {
            this.hasInited = false;
            this.$i18n.locale = await AppHelper.langCodeGet();
            await Dictionary.load();

            document.title = this.$t('app.menu.tool.wsServer');

            // load options
            let options = await MdbRuntimeVariable.getVarValue('tool_websocket_server_options', '');
            if ( '' != options ) {
                options = JSON.parse(options);
            } else {
                options = {};
            }
            this.options = options;
            
            // init options
            this.protocol = this.options.protocol || this.protocol;
            this.host = this.options.host || this.host;
            this.port = this.options.port || this.port;
            this.path = this.options.path || this.path;
            this.charset = this.options.charset || this.charset;
            this.viewmode = this.options.viewmode || this.viewmode;
            this.sslKey = this.options.sslKey || this.sslKey;
            this.sslCert = this.options.sslCert || this.sslCert;
            this.enableDataMerge = true;
            if ( false === this.options.enableDataMerge ) {
                this.enableDataMerge = false;
            }

            this.hasInited = true;
            this.$warning({
                title: 'WARNING',
                content: this.$t('app.toolWsServer.offlineTip'),
            });
        },

        /**
         * event handler on option updated
         * @param {Object|null} newOptions
         */
        async actionOptionUpdate( newOptions ) {
            if ( null == newOptions ) {
                newOptions = Common.objCopy(this.options);
            }
            newOptions.protocol = this.protocol;
            newOptions.host = this.host;
            newOptions.port = this.port;
            newOptions.path = this.path;
            newOptions.charset = this.charset;
            newOptions.viewmode = this.viewmode;
            newOptions.sslKey = this.sslKey;
            newOptions.sslCert = this.sslCert;
            newOptions.enableDataMerge = this.enableDataMerge;
            this.options = newOptions;
            await MdbRuntimeVariable.setVarValue('tool_websocket_server_options', JSON.stringify(newOptions));
            this.$forceUpdate();
        },

        /**
         * update option on setting ok
         */
        actionSettingOk( options ) {
            this.sslKey = options.sslKey;
            this.sslCert = options.sslCert;
            this.options.sslKey = options.sslKey;
            this.options.sslCert = options.sslCert;
            this.actionOptionUpdate(null);
        },

        /**
         * start server
         */
        async actionStart() {
            try {
                let wsOptions = {};
                wsOptions.path = '/' + this.path;
                if ( 'wss' === this.protocol ) {
                    wsOptions.server = await this.setupHttpsServer();
                } else {
                    wsOptions.host = this.host;
                    wsOptions.port = this.port;
                }
                this.server = new window.ws.WebSocketServer(wsOptions);
                this.server.on('connection', (ws) => this.handleOnConnection(ws));
                this.isListening = true;
                this.$message.success(this.$t('app.toolWsServer.serverStarted'), 1);
            } catch ( e ) {
                this.$message.error(e.message);
            }
        },
        
        /**
         * setup https server
         * @returns <Promise>
         */
        setupHttpsServer() {
            let $this = this;
            return new Promise(( resolve ) => {
                let credentials = {};
                credentials.key = $this.sslKey;
                credentials.cert = $this.sslCert;
                $this.httpsServer = window.https.createServer(credentials);
                $this.httpsServer.listen({port:$this.port}, () => {
                    resolve($this.httpsServer);
                });
            });
        },

        /**
         * event handler on client conented
         * @param {Object} ws
         */
        handleOnConnection( ws ) {
            let key = `${ws._socket.remoteAddress}:${ws._socket.remotePort}`;
            if ( 0 == Object.keys(this.clients).length ) {
                this.activeClientKey = key;
            }

            this.clients[key] = {
                ws : ws,
                data : [],
                hasNewData : false,
                isConnected : true,
            };
            ws.on('message', (data) => this.handleOnMessage(key, data) );
            ws.on('close', () => this.handleClientClose(key) );
            this.$forceUpdate();
        },

        /**
         * event handle on server receive client data
         * @param {String} clientKey
         * @param {Buffer} data
         */
        handleOnMessage(clientKey, data) {
            this.$refs[`dataViewer:${clientKey}`][0].dataReceive(data);
            this.$refs.mockResponseHandler.setRequestData(clientKey, data);
            if ( this.activeClientKey != clientKey ) {
                this.clients[clientKey].hasNewData = true;
            }

            this.$forceUpdate();
        },

        /**
         * event handle on client disconnected
         * @param {String} key
         */
        handleClientClose(key) {
            if ( undefined == this.clients[key] ) {
                return ;
            }
            console.log(`client [${key}] is closed`);
            this.clients[key].isConnected = false;

            if ( key == this.activeClientKey ) {
                this.activeClientKey = null;
                let clientKeys = Object.keys(this.clients);
                if ( 0 < clientKeys.length ) {
                    this.activeClientKey = clientKeys[0];
                }
            }

            this.$forceUpdate();
        },
        
        /**
         * stop server
         */
        actionStop() {
            let $this = this;
            for ( let key in this.clients ) {
                this.clients[key].ws.terminate();
            }
            this.clients = [];

            if ( null !== this.server ) {
                this.server.close(function() {
                    $this.isListening = false;
                    $this.$message.success($this.$t('app.toolWsServer.serverStoped'),1);
                });
            }

            if ( null !== this.httpsServer ) {
                this.httpsServer.close();
                this.httpsServer = null;
            }
            this.activeClientKey = null;
        },

        /**
         * event handler on mock response generated
         * @param {String} data
         */
        actionMockResponseGenerated( data ) {
            let response = null;
            if ( 'text' === data.mode ) {
                response = Common.charsetConvert(data.content, this.charset, 'utf8');
                response = Buffer.from(response);
            } else {
                response = Buffer.from(Common.convertStringToHex(data.content));
            }
            
            if ( null === data.clientKey ) {
                data.clientKey = this.activeClientKey;
            }
            if ( undefined == this.clients[data.clientKey] ) {
                return;
            }
            this.$refs[`dataViewer:${data.clientKey}`][0].dataSend(response, data.note);
            let ws = this.clients[data.clientKey].ws;
            ws.send(response, {binary:true});
        },

        /**
         * event handler on client tab edit
         * @param {String} targetKey
         * @param {Object} action
         */
        actionClientTabEdit( targetKey, action ) {
            action = action[0].toUpperCase() + action.substr(1);
            let handler = `handleClientTabAction${action}`;
            this[handler](targetKey);
        },

        /**
         * event handler on client tab remove
         * @param {String} key
         */
        handleClientTabActionRemove( key ) {
            this.clients[key].ws.terminate();
        },

        /**
         * event handler on client tab changed
         * @param {String} tabKey
         */
        actionClientTabChange( tabKey ) {
            this.activeClientKey = tabKey;
            this.clients[tabKey].hasNewData = false;
            this.$forceUpdate();
        },

        /**
         * event handle on data size changed
         * @param {Number} size
         * @param {String} dir
         * @param {String} clientKey
         */
        actionDataSizeChanged( size, dir, clientKey ) {
            if ( this.activeClientKey != clientKey ) {
                return;
            }
            this[dir] = size;
            this.$forceUpdate();
        },

        /**
         * reset data size send
         */
        actionDataSizeSendReset() {
            this.$refs[`dataViewer:${this.activeClientKey}`][0].dataSizeSendReset();
            this.$forceUpdate();
        },

        /**
         * reset data size receive
         */
        actionDataSizeReceiveReset() {
            this.$refs[`dataViewer:${this.activeClientKey}`][0].dataSizeReceiveReset();
            this.$forceUpdate();
        },
    },
}
</script>
<style>
#clienttab .ant-tabs-content {height: 100% !important;}
#clienttab .ant-tabs-tabpane-active {height: 100% !important;}
#clienttab .ant-tabs-right-content {padding-right: 2px !important;}
</style>